/*
 *  Copyright 2007 Genoworks
 *  
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *  
 *      http://www.apache.org/licenses/LICENSE-2.0
 *  
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License. 
 */

package com.genoworks.retriever.client;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 */
public class RetrieverClient {

  private String hostAddress = null;

  private int port = 1234;

  private long totalRequestTime = 0L;

  private long totalResponseTime = 0L;

  private int totalNumberOfTransactions = 0;

  /**
   * 특정 서버에 접속해 요청을 전송할 수 있는
   * {@link RetrieverClient} 인스턴스를 생성한다.
   * 
   * @param addr 호스트 주소
   * @param p 포트
   */
  public RetrieverClient( String addr, int p ) {
    hostAddress = addr;
    port = p;
  }

  /**
   * 현재 접속하도록 설정되어 있는 호스트 주소
   * 
   * @return
   */
  public String getHostAddress() {
    return hostAddress;
  }

  /**
   * 현재 접속하도록 설정되어 있는 포트 번호
   * 
   * @return
   */
  public int getPort() {
    return port;
  }

  /**
   * 지정한 XML 요청을 전송하고, 그 결과를 XML DOM의
   * 형태로 알려준다.
   * 
   * @param xml
   * @return
   * @throws IOException
   * @throws ParserConfigurationException
   * @throws SAXException
   */
  private Document toDom( byte[] xml ) throws IOException, ParserConfigurationException,
      SAXException {

    //
    DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    return builder.parse( new InputSource( new ByteArrayInputStream( xml ) ) );
  }

  /**
   * @param xml
   * @return
   * @throws IOException
   */
  private ResponseValues requestInternal( byte[] xml ) throws IOException {

    //
    long startAt = System.currentTimeMillis();

    /**
     */
    URL url = new URL( "http://" + hostAddress + ( port != 80 ? ":" + port : "" ) + "/xml-api" );
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();

    /**
     */
    conn.setRequestMethod( "POST" );
    conn.setDoOutput( true );

    /**
     */
    OutputStream os = conn.getOutputStream();
    os.write( xml );
    os.flush();

    //
    totalRequestTime += ( System.currentTimeMillis() - startAt );

    /**
     * Here comes the result.
     */
    startAt = System.currentTimeMillis();

    //
    String charSet = null;
    String contentType = conn.getContentType();
    if ( contentType != null ) {
      contentType = contentType.toLowerCase();
      int idx = contentType.indexOf( "charset=" );
      if ( idx >= 0 ) {
        charSet = contentType.substring( idx + 8 );
      }
    }
    if ( charSet == null ) {
      charSet = "UTF-8";
    }

    /**
     */
    ResponseValues result = new ResponseValues();

    //
    ByteArrayOutputStream resultBytes = new ByteArrayOutputStream();
    BufferedInputStream resultInput = new BufferedInputStream( conn.getInputStream() );
    try {
      byte[] byteBuffer = new byte[ 10 * 1024 ];
      int byteBufferLen = 0;
      while ( ( byteBufferLen = resultInput.read( byteBuffer ) ) >= 0 ) {
        resultBytes.write( byteBuffer, 0, byteBufferLen );
      }
    } finally {
      resultInput.close();
    }
    result.rawResponseBytes = resultBytes.toByteArray();

    /**
     */
    StringBuffer resultOut = new StringBuffer();
    BufferedReader in = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( result.rawResponseBytes ),
        charSet ) ); // FIXED Issue#5
    try {
      char[] buf = new char[ 1024 ];
      int bufLen = 0;
      while ( ( bufLen = in.read( buf ) ) > 0 ) {
        resultOut.append( buf, 0, bufLen );
      }
    } finally {
      in.close();
    }
    result.rawResponse = resultOut.toString();

    //
    totalResponseTime += ( System.currentTimeMillis() - startAt );

    //
    totalNumberOfTransactions += 1;

    //
    conn.disconnect();

    //
    return result;
  }

  /**
   * 요청을 전송하는 데 걸린 평균 시간. 단위는
   * 밀리초(msec.).
   * 
   * @return
   */
  public long getAverageRequestTimeTaken() {
    return totalRequestTime / totalNumberOfTransactions;
  }

  /**
   * 요청한 이후 결과를 받아오는 데 걸린 평균 시간. 단위는
   * 밀리초(msec.).
   * 
   * @return
   */
  public long getAverageResponseTimeTaken() {
    return totalResponseTime / totalNumberOfTransactions;
  }

  /**
   * 한번 요청해 결과를 받아오는 데 걸린 평균 시간. 단위는
   * 밀리초(msec.).
   * 
   * @return
   */
  public long getAverageTransactionTimeTaken() {
    return ( totalRequestTime + totalResponseTime ) / totalNumberOfTransactions;
  }

  /**
   * 요청하고 응답을 받았던 횟수.
   * 
   * @return
   */
  public int getTotalNumberOfTransactions() {
    return totalNumberOfTransactions;
  }

  public BaseResult request( String requestXML ) throws IOException, ParserConfigurationException,
      SAXException {
    return request( requestXML, null );
  }

  public BaseResult request( String requestXML, String encoding ) throws IOException,
      ParserConfigurationException, SAXException {

    // Issue#1
    // Prevent illegal XML characters from being sent to server
    requestXML = filterIllegalCharacters( requestXML );

    //
    byte[] xmlBytes = null;
    if ( encoding == null ) {
      xmlBytes = requestXML.getBytes( "UTF-8" );
    } else {
      xmlBytes = requestXML.getBytes( encoding );
    }

    //
    final ResponseValues responseXML = requestInternal( xmlBytes );
    Document domResult = toDom( responseXML.rawResponseBytes );

    //
    NodeList docList = domResult.getElementsByTagName( "docs" );
    if ( docList.getLength() > 0 ) {

      /**
       * it is a result of a search
       * request!!
       */
      SearchResult result = new SearchResult( domResult );
      result.setRawRequest( requestXML );
      result.setRawReply( responseXML.rawResponse );
      result.setRawReplyBytes( responseXML.rawResponseBytes );

      //
      fillSearchResult( domResult, result );

      //
      return result;
    }

    //
    BaseResult result = new BaseResult( domResult );
    result.setRawRequest( requestXML );
    result.setRawReply( responseXML.rawResponse );
    result.setRawReplyBytes( responseXML.rawResponseBytes );
    return result;
  }

  public SearchResult search( String requestXML ) throws IOException, ParserConfigurationException,
      SAXException {
    return search( requestXML, null );
  }

  public SearchResult search( String requestXML, String encoding ) throws IOException,
      ParserConfigurationException, SAXException {

    // Issue#1
    // Prevent illegal XML characters from being sent to server
    requestXML = filterIllegalCharacters( requestXML );

    //
    byte[] xmlBytes = null;
    if ( encoding == null ) {
      xmlBytes = requestXML.getBytes( "UTF-8" );
    } else {
      xmlBytes = requestXML.getBytes( encoding );
    }

    //
    ResponseValues response = requestInternal( xmlBytes );
    Document domResult = toDom( response.rawResponseBytes );

    //
    SearchResult result = new SearchResult( domResult );
    result.setRawRequest( requestXML );
    result.setRawReply( response.rawResponse );
    result.setRawReplyBytes( response.rawResponseBytes );

    //
    fillSearchResult( domResult, result );

    //
    return result;
  }

  /**
   * Issue#1 (Prevent illegal XML
   * characters from being sent to
   * server)
   * 
   * @param str
   * @return
   */
  private String filterIllegalCharacters( String str ) {

    /*
     * Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
     */

    //
    final StringBuilder result = new StringBuilder();

    //
    for ( int i = 0; i < str.length(); i++ ) {
      final char c = str.charAt( i );
      if ( ( c < 0x20 && c != 0x9 && c != 0xA && c != 0xD )
          || ( c > 0xD7FF && c < 0xE000 )
          || ( c > 0xFFFD && c < 0x10000 )
          || c > 0x10FFFF ) {
        result.append( ' ' );
      } else {
        result.append( c );
      }
    }

    //
    return result.toString();
  }

  private void fillSearchResult( Document domResult, SearchResult result ) {

    /*
     * 1. documents
     */

    // attributes
    NodeList list = domResult.getElementsByTagName( "docs" );
    if ( list.getLength() > 0 ) {
      result.setTotalNumberOfResults( getAttributeInteger( list.item( 0 ), "total-count" ) );
      result.setTotalNumberOfPages( getAttributeInteger( list.item( 0 ), "total-pages" ) );
    }

    // entries
    list = domResult.getElementsByTagName( "doc" );
    for ( int i = 0; i < list.getLength(); i++ ) {
      NodeList children = list.item( i ).getChildNodes();
      LinkedHashMap<String, String> nd = new LinkedHashMap<String, String>();
      for ( int f = 0; f < children.getLength(); f++ ) {
        Node child = children.item( f );
        if ( child.getNodeType() != Node.ELEMENT_NODE
            && child.getNodeName().equalsIgnoreCase( "field" ) == false ) {
          continue;
        }
        final String fieldName = getAttributeText( child, "name" );
        final String fieldText = result.getNodeText( child );
        nd.put( fieldName, fieldText );
      }

      //
      Float scoreObj = getAttributeFloat( list.item( i ), "score" );
      float score = 0.0F;
      if ( scoreObj != null ) {
        score = scoreObj.floatValue();
      }

      //
      result.addEntry( nd, score );
    }

    /*
     * 2. facets, if exist
     */
    list = domResult.getElementsByTagName( "facets" );
    if ( list != null && list.getLength() > 0 ) {
      final int len = list.getLength();
      for ( int i = 0; i < len; i++ ) {
        Node facets = list.item( i );
        final String id = getAttributeText( facets, "id" );
        final String field = getAttributeText( facets, "field" );
        NodeList facetList = facets.getChildNodes();
        for ( int f = 0; f < facetList.getLength(); f++ ) {
          Node oneFacet = facetList.item( f );
          Number countNum = getAttributeInteger( oneFacet, "count" );
          int count = 0;
          if ( countNum != null ) {
            count = countNum.intValue();
          } else {
            count = -1;
          }
          final String name = getAttributeText( oneFacet, "name" );
          if ( name != null && count >= 0 ) {
            result.addFacet( id, field, name, count );
          }
        }
      }
    }

    /*
     * 3. cluster keywords
     */
    final Map<String, List<String>> contents = new HashMap<String, List<String>>();
    list = domResult.getElementsByTagName( "content-list" );
    if ( list != null && list.getLength() > 0 ) {
      final int len = list.getLength();
      for ( int i = 0; i < len; i++ ) {
        final Node listItem = list.item( i );
        final String content = result.getNodeText( listItem );
        final List<String> strList = new LinkedList<String>();
        for ( String contentEntry : content.split( ":" ) ) {
          strList.add( contentEntry );
        }
        contents.put( getAttributeText( listItem, "id" ), strList );
      }
    }
    //
    list = domResult.getElementsByTagName( "keyword" );
    if ( list != null && list.getLength() > 0 ) {
      final int len = list.getLength();
      for ( int i = 0; i < len; i++ ) {
        final Node keyword = list.item( i );
        final String keywordStr = result.getNodeText( keyword );
        final String id = getAttributeText( keyword, "id" );
        if ( keywordStr != null && keywordStr.length() > 0 ) {
          result.addCluster( id, keywordStr, getAttributeInteger( keyword, "count" ) );
          if ( contents.containsKey( id ) ) {
            result.setClusterContentList( id, contents.get( id ) );
          }
        }
      }
    }

    /*
     * 4. query key
     */
    list = domResult.getElementsByTagName( "query-key" );
    if ( list != null && list.getLength() > 0 ) {
      Node queryKey = list.item( 0 );
      final String queryKeyStr = result.getNodeText( queryKey );
      if ( queryKeyStr != null && queryKeyStr.length() > 0 ) {
        result.setQueryKey( queryKeyStr );
      }
    }
  }

  private String getAttributeText( Node node, String attributeName ) {
    NamedNodeMap map = node.getAttributes();
    if ( map == null ) {
      return null;
    }
    final int length = map.getLength();
    for ( int i = 0; i < length; i++ ) {
      Node attribute = map.item( i );
      if ( attribute.getNodeName().equalsIgnoreCase( attributeName ) ) {
        return attribute.getNodeValue();
      }
    }
    return null;
  }

  private Integer getAttributeInteger( Node n, String attributeName ) {
    try {
      return Integer.parseInt( getAttributeText( n, attributeName ) );
    } catch( Exception e ) {
    }
    return null;
  }

  private Float getAttributeFloat( Node n, String attributeName ) {
    try {
      return Float.parseFloat( getAttributeText( n, attributeName ) );
    } catch( Exception e ) {
    }
    return null;
  }

  @SuppressWarnings( "nls" )
  public static final String getVersionString() throws IOException {
    final String jarName = "retriever-client.jar";
    final String keyVersion = "Version";
    StringTokenizer st = new StringTokenizer( System.getProperty( "java.class.path" ), ":" );
    while ( st.hasMoreTokens() ) {
      final String path = st.nextToken();
      if ( path.endsWith( jarName ) ) {
        JarFile jar = new JarFile( path );
        Manifest manifest = jar.getManifest();
        Attributes attribs = manifest.getMainAttributes();
        for ( Object obj : attribs.keySet() ) {
          if ( obj.toString().equalsIgnoreCase( keyVersion ) ) {
            return attribs.getValue( keyVersion );
          }
        }
        throw new RuntimeException( "version information not found in the " + jarName + " file." );
      }
    }
    throw new RuntimeException( jarName + " file not found on class path." );
  }

  private class ResponseValues {

    byte[] rawResponseBytes = null;

    String rawResponse = null;

  }
}
